package run.iget.security.interceptor;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import run.iget.framework.common.enums.BaseResultEnum;
import run.iget.framework.common.util.ExceptionThrowUtils;
import run.iget.framework.common.util.WebUtils;
import run.iget.framework.event.EventPublishUtils;
import run.iget.security.annotation.AuthCheck;
import run.iget.security.bean.SafeAuthOptionLog;
import run.iget.security.bean.SafeAuthUser;
import run.iget.security.config.SecurityProperties;
import run.iget.security.constant.SecurityConst;
import run.iget.security.convert.SafeAuthOptionLogConvert;
import run.iget.security.event.SafeAuthOptionLogEvent;
import run.iget.security.util.LoginUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Date;
import java.util.Objects;
import java.util.Set;

/**
 * 代码千万行，注释第一行，注释不规范，迭代两行泪
 * ---------------类描述-----------------
 * 资源权限拦截器
 * ---------------类描述-----------------
 *
 * @author 大周
 * @since 2022/8/24 10:12
 */
@Slf4j
public class AuthCheckInterceptor implements HandlerInterceptor {

    private SecurityProperties properties;

    private final AntPathMatcher pathMatcher = new AntPathMatcher();
    private final ThreadLocal<SafeAuthOptionLog> SAFE_AUTH_OPTION_LOG_LOCAL = new ThreadLocal();

    public AuthCheckInterceptor(SecurityProperties properties) {
        this.properties = properties;
    }


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object controller)
            throws Exception {
        if (Objects.isNull(properties) || properties.notEnabled()) {
            // 如果配置为空，或未启用，则直接放行
            return true;
        }

        // 获取权限校验的注解对象
        AuthCheck authCheck = getResourceAuth(controller);
        // 获取token
        String token = WebUtils.getParameter(SecurityConst.TOKEN_KEY);
        if (Objects.nonNull(authCheck)) {
            /**
             * 解决标记了AuthCheck注解后，不需要校验token，但又想获取token对应的用户信息
             * 场景：接口标注了 AuthCheck(enable=false)
             * 但此接口无论登录与否都可以访问，只是结果数据不同
             * 此时仅通过token获取登录用户信息，无论用户信息是否为空
             */
            SafeAuthUser loginUser = LoginUtils.getByToken(token);
            LoginUtils.set(loginUser);
        }

        // 如果不需要token校验，则直接放行
        if (!this.needCheckToken(request, authCheck)) {
            return true;
        }

        // 对token校验
        ExceptionThrowUtils.ofBlank(token, BaseResultEnum.ERROR_LOGIN);

        // 根据token获取登录的用户信息
        SafeAuthUser loginUser = LoginUtils.getByToken(token);
        ExceptionThrowUtils.ofNull(loginUser, BaseResultEnum.ERROR_AUTH);
        // 记录当前的账号
        LoginUtils.set(loginUser);

        // 记录操作日志
        this.genOptionLog(request, authCheck, loginUser);

        // 如果不需要校验uri则直接放行
        if (!this.needCheckUri(request, authCheck)) {
            return true;
        }

        // 二次认证校验 header中携带密码
        if (authCheck.enable() && authCheck.safeCheck()) {
            String password = WebUtils.getParameter(SecurityConst.SAFE_KEY);
            ExceptionThrowUtils.ofNotTrue(loginUser.isSafeValue(password), BaseResultEnum.ERROR_AUTH_SAFE);
        }

        // 如果没有权限，则抛出未授权
        boolean hasAuth = this.hasAuth(request, loginUser.getPermissions());
        ExceptionThrowUtils.ofFalse(hasAuth, BaseResultEnum.ERROR_AUTH);

        // 判断是否必须有某个角色的权限
        Set<Long> roleIds = LoginUtils.getRoleIds();
        long[] requiredRoleIds = authCheck.requiredRoleIds();
        if (Objects.nonNull(requiredRoleIds) && requiredRoleIds.length > 0) {
            boolean hasRole = Arrays.stream(requiredRoleIds).anyMatch(item -> roleIds.contains(item));
            ExceptionThrowUtils.ofFalse(hasRole, BaseResultEnum.ERROR_AUTH);
        }

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        pushOptionLog(response);
        LoginUtils.clear();
    }

    /**
     * 获取Controller类或方法的注解
     *
     * @param handler controller类对象
     * @return
     */
    private AuthCheck getResourceAuth(Object handler) {
        if (!(handler instanceof HandlerMethod)) {
            return null;
        }
        // 转为controller类
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        // 获取方法上的注解
        AuthCheck resourceAuth = handlerMethod.getMethod().getAnnotation(AuthCheck.class);

        if (Objects.isNull(resourceAuth)) {
            // 如果方法上注解为空，再进一步判断类上是否有注解
            resourceAuth = handlerMethod.getClass().getAnnotation(AuthCheck.class);
        }

        if (Objects.isNull(resourceAuth)) {
            // 如果方法上注解为空，再进一步判断类上是否有注解
            resourceAuth = handlerMethod.getBeanType().getAnnotation(AuthCheck.class);
        }

        return resourceAuth;
    }

    /**
     * 判断是否需要进行token校验
     *
     * @param request
     * @param authCheck
     * @return
     */
    private boolean needCheckToken(HttpServletRequest request, AuthCheck authCheck) {
        boolean needCheckToken = false;
        if (CollUtil.isNotEmpty(properties.getNeedLogin())) {
            needCheckToken = properties.getNeedLogin().stream().anyMatch(
                    path -> pathMatcher.match(path, request.getRequestURI()) || request.getRequestURI().endsWith(path));
        }
        return needCheckToken || (Objects.nonNull(authCheck) && authCheck.enable());
    }

    /**
     * 生成操作记录
     *
     * @param request
     * @param authUser
     */
    private void genOptionLog(HttpServletRequest request, AuthCheck authCheck, SafeAuthUser authUser) {
        if (Objects.isNull(request) || Objects.isNull(authUser) || Objects.isNull(authCheck)) {
            return;
        }
        SafeAuthOptionLog safeAuthOptionLog = SafeAuthOptionLogConvert.I.to(authUser);
        safeAuthOptionLog.setCategory(authCheck.optionCategory());
        safeAuthOptionLog.setPage(authCheck.optionPage());
        safeAuthOptionLog.setContent(authCheck.optionContent());
        safeAuthOptionLog.setRequestUri(request.getRequestURI());
        safeAuthOptionLog.setIp(WebUtils.getClientIp(request));
        safeAuthOptionLog.setRequestParams(JSON.toJSONString(WebUtils.getParameterMap(request)));
        safeAuthOptionLog.setRequestType(request.getMethod());
        SAFE_AUTH_OPTION_LOG_LOCAL.set(safeAuthOptionLog);
    }

    /**
     * 发布操作记录
     */
    private void pushOptionLog(HttpServletResponse response) {
        SafeAuthOptionLog safeAuthOptionLog = SAFE_AUTH_OPTION_LOG_LOCAL.get();
        SAFE_AUTH_OPTION_LOG_LOCAL.remove();
        if (Objects.isNull(safeAuthOptionLog)) {
            return;
        }
        safeAuthOptionLog.setEndTime(new Date());
        safeAuthOptionLog.setCastTime(DateUtil.betweenMs(safeAuthOptionLog.getStartTime(), safeAuthOptionLog.getEndTime()));
        safeAuthOptionLog.setResponseCode(String.valueOf(response.getStatus()));
        EventPublishUtils.publish(new SafeAuthOptionLogEvent(safeAuthOptionLog));
    }

    /**
     * 判断是否需要校验uri
     *
     * @param request
     * @param authCheck
     * @return
     */
    private boolean needCheckUri(HttpServletRequest request, AuthCheck authCheck) {
        if (Objects.nonNull(properties.getMockAdminId())) {
            SafeAuthUser loginUser = new SafeAuthUser();
            loginUser.setId(properties.getMockAdminId());
            loginUser.setUsername("mock_" + properties.getMockAdminId());
            if (Objects.nonNull(properties.getMockAdminId())) {
                loginUser.setRoles(Sets.newHashSet(properties.getMockRoleId()));
            }
            LoginUtils.set(loginUser);
            return false;
        }
        boolean needCheck = false;
        if (CollUtil.isNotEmpty(properties.getNeedPermissions())) {
            needCheck = properties.getNeedPermissions().stream()
                    .anyMatch(path -> pathMatcher.match(path, request.getRequestURI()));
        }
        return needCheck || (Objects.nonNull(authCheck) && authCheck.enable() && authCheck.checkUri());
    }

    /**
     * 判断是否有权限
     *
     * @param request
     * @param permissions
     * @return
     */
    private boolean hasAuth(HttpServletRequest request, Set<String> permissions) {
        if (CollUtil.isEmpty(permissions)) {
            return false;
        }
        return permissions.contains(request.getServletPath()) || permissions.contains(request.getRequestURI());
    }
}
